﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;

namespace GeneralizedEnum
{
    [DebuggerDisplay("{Name} / {Value} / {Label}")]
    public abstract class Enumerated<TThis, TBase> : IXmlSerializable
        where TThis : Enumerated<TThis, TBase>
    {
        // Actual value
        public TBase Value { get; private set; }

        // Label for the UI.
        public string Label { get; private set;}

        // Name of the field, as used in code.
        private string name;

        public string Name {
            get
            {
                InitValues();
                return name;
            }
            private set
            {
                name = value;
            }
        }

        protected Enumerated(string label, TBase value)
            : this()
        {
            this.Label = label;
            this.Value = value;
        }

        // Parameterless constructor required for XML serialization.
        protected Enumerated()
        {
        }

        public override string ToString()
        {
            return Label;
        }

        protected internal virtual void Assign(Enumerated<TThis, TBase> other)
        {
            this.Name = other.Name;
            this.Value = other.Value;
            this.Label = other.Label;
        }

        #region Comparison

        public override bool Equals(object obj)
        {
            if (obj == null)
                return false;
            else if (obj.GetType() != this.GetType())
                return false;
            return object.Equals(((Enumerated<TThis, TBase>)obj).Value, Value);
        }

        public static bool operator==(Enumerated<TThis, TBase> left, Enumerated<TThis, TBase> right)
        {
            return object.Equals(left, right);
        }

        public static bool operator !=(Enumerated<TThis, TBase> left, Enumerated<TThis, TBase> right)
        {
            return !object.Equals(left, right);
        }

        public override int GetHashCode()
        {
            return Value.GetHashCode();
        }

        #endregion

        #region Enumeration of values

        // Static properties are lazily initialized, so you need to call InitValues on the first access of either Name or Values.
        private static IDictionary<TBase, TThis> values;
        private static IDictionary<string, TThis> names;

        private static void InitValues()
        {
            if (values != null)
                return;

            var newValues = new Dictionary<TBase, TThis>();
            var newNames = new Dictionary<string, TThis>();

            // Look for all non-inherited fields (that is, member variables) that are public and static.
            FieldInfo[] staticFields = typeof(TThis).GetFields(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static);
            foreach (FieldInfo field in staticFields)
            {
                // Ignore fields that aren't of the correct type or read-only
                if (field.FieldType != typeof(TThis) || (field.Attributes & FieldAttributes.InitOnly) == (FieldAttributes)0)
                    continue;

                // Retrieve the value of the field.
                // No object is required, because it's a static field.
                TThis value = (TThis)field.GetValue(null);

                // Save the field's name, as defined in code.
                // Use the field rather than the property in order to prevent recursion.
                value.name = field.Name;

                // Store it in the list for later.
                newValues.Add(value.Value, value);
                newNames.Add(value.name, value);
            }
            values = newValues;
            names = newNames;
        }

        public static IEnumerable<TThis> Values
        {
            get
            {
                InitValues();
                return values.Values;
            }
        }

        #endregion

        #region Conversion

        public static explicit operator TBase(Enumerated<TThis, TBase> obj)
        {
            return obj.Value;
        }

        public static explicit operator Enumerated<TThis, TBase>(TBase obj)
        {
            try
            {
                return values[obj];
            }
            catch (KeyNotFoundException ex)
            {
                // Expected error: the code is unknown. In this case, say 'invalid value' rather than the cryptic, implementation-oriented 'key not found'.
                var errorMessage = string.Format("Conversion from {0} to {1} failed: '{2}' is not a valid value.", typeof(TBase), typeof(TThis), obj);
                throw new InvalidCastException(errorMessage, ex);
            }
        }

        TThis FromName(string name)
        {
            try
            {
                return names[name];
            }
            catch (KeyNotFoundException ex)
            {
                // Say 'invalid value' rather than the cryptic, implementation-oriented 'key not found'.
                var errorMessage = string.Format("There is no {0} value with the name '{1}'.", typeof(TThis), name);
                throw new InvalidCastException(errorMessage, ex);
            }
        }

        #endregion

        #region IXmlSerializable Members

        XmlSchema IXmlSerializable.GetSchema()
        {
            return null;
        }

        void IXmlSerializable.ReadXml(XmlReader reader)
        {
            var name = reader.ReadElementContentAsString();
            var value = FromName(name);
            this.Assign(value);
        }

        void IXmlSerializable.WriteXml(XmlWriter writer)
        {
            writer.WriteString(this.Name);
        }

        #endregion

        #region Misc

        public virtual bool In(params TThis[] values)
        {
            return In((IList<TThis>)values);
        }

        public virtual bool In(IList<TThis> values)
        {
            return values.Contains((TThis)this);
        }

        #endregion
    }
}
